# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
from odoo.exceptions import UserError
from collections import defaultdict

class StockMove(models.Model):
    _inherit = "stock.move"
    reserved_availability = fields.Float(
        'Quantity Reserved', compute='_compute_reserved_availability',
        digits='Product Unit of Measure',
        readonly=True, help='Quantity that has already been reserved for this move')
    quantity_done = fields.Float(
        'Quantity Done', compute='_quantity_done_compute', digits='Product Unit of Measure',
        inverse='_quantity_done_set', store=True)
    move_line_nosuggest_ids = fields.One2many('stock.move.line', 'move_id',
                                              domain=['|', ('quantity', '=', 0.0), ('qty_done', '!=', 0.0)])
    # RMAs that were created from the delivery move
    rma_ids = fields.One2many(
        comodel_name="rma",
        inverse_name="move_id",
        string="RMAs",
        copy=False,
    )
    # RMAs linked to the incoming movement from client
    rma_receiver_ids = fields.One2many(
        comodel_name="rma",
        inverse_name="reception_move_id",
        string="RMA receivers",
        copy=False,
    )
    # RMA that create the delivery movement to the customer
    rma_id = fields.Many2one(
        comodel_name="rma",
        string="RMA return",
        copy=False,
    )

    def _get_move_lines(self):
        """ This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
        In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line."""
        self.ensure_one()
        if self.picking_type_id.show_reserved is False:
            return self.move_line_nosuggest_ids
        return self.move_line_ids

    def _quantity_done_sml(self):
        lines = self._get_move_lines()
        q = 0
        for l in lines:
            q = l.qty_done+q
        return q

    @api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done')
    def _quantity_done_compute(self):
        """ This field represents the sum of the move lines `qty_done`. It allows the user to know
        if there is still work to do.

        We take care of rounding this value at the general decimal precision and not the rounding
        of the move's UOM to make sure this value is really close to the real sum, because this
        field will be used in `_action_done` in order to know if the move will need a backorder or
        an extra move.
        """
        if not any(self._ids):
            # onchange
            for move in self:
                move.quantity_done = move._quantity_done_sml()
        else:
            # compute
            move_lines_ids = set()
            for move in self:
                move_lines_ids |= set(move._get_move_lines().ids)

            data = self.env['stock.move.line']._read_group(
                [('id', 'in', list(move_lines_ids))],
                ['move_id', 'product_uom_id'], ['quantity:sum']
            )
            sum_qty = defaultdict(float)
            for move, product_uom, qty_sum in data:
                uom = move.product_uom
                sum_qty[move.id] += product_uom._compute_quantity(qty_sum, uom, round=False)

            for move in self:
                move.quantity_done = sum_qty[move.id]

    def _quantity_done_set(self):
        def _process_decrease(move, quantity):
            """Prioritize decrease the ml without reserved qty"""
            res_mls = move._get_move_lines().sorted(
                lambda ml: float_is_zero(ml.reserved_uom_qty, precision_rounding=ml.product_uom_id.rounding),
                reverse=True)
            qty_to_unreserve = move.reserved_availability - move.product_uom_qty
            for ml in res_mls:
                if float_is_zero(quantity, precision_rounding=move.product_uom.rounding):
                    break
                qty_ml_dec = min(ml.qty_done,
                                 ml.product_uom_id._compute_quantity(quantity, ml.product_uom_id, round=False))
                if float_is_zero(qty_ml_dec, precision_rounding=ml.product_uom_id.rounding):
                    continue
                ml.qty_done -= qty_ml_dec
                quantity -= move.product_uom._compute_quantity(qty_ml_dec, move.product_uom, round=False)
                if not ml.exists():
                    # If decreasing the move line qty_done to 0 let it to be unlinked (i.e. for immediate transfers)
                    continue
                # Unreserve
                immediate_transfer = move.state in ['done']
                if (not immediate_transfer and move.reserved_availability < move.product_uom_qty):
                    continue
                if float_compare(ml.reserved_uom_qty, ml.qty_done, precision_rounding=ml.product_uom_id.rounding) <= 0:
                    continue
                if immediate_transfer:
                    ml.reserved_uom_qty = ml.qty_done
                elif float_compare(qty_to_unreserve, 0, precision_rounding=move.product_uom.rounding) > 0:
                    qty_unreserved = min(qty_to_unreserve, ml.reserved_qty - ml.qty_done)
                    ml.reserved_uom_qty = ml.reserved_qty - qty_unreserved
                    qty_to_unreserve -= qty_unreserved

        def _process_increase(move, quantity):
            moves = move
            #if move.picking_id.immediate_transfer:
            moves = move._action_confirm(merge=False)
            # Kits, already handle in action_explode, should be clean in master
            if len(moves) > 1:
                return
            if move.reserved_availability < move.quantity_done and move.state not in ['done', 'cancel']:
                move._action_assign(force_qty=move.quantity_done)
            move._set_quantity_done(quantity)

        err = []
        for move in self:
            uom_qty = float_round(move.quantity_done, precision_rounding=move.product_uom.rounding,
                                  rounding_method='HALF-UP')
            precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
            qty = float_round(move.quantity_done, precision_digits=precision_digits, rounding_method='HALF-UP')
            if float_compare(uom_qty, qty, precision_digits=precision_digits) != 0:
                err.append(_("""
    The quantity done for the product %s doesn't respect the rounding precision defined on the unit of measure %s.
    Please change the quantity done or the rounding precision of your unit of measure.""",
                             move.product_id.display_name, move.product_uom.display_name))
                continue
            delta_qty = move.quantity_done - move._quantity_done_sml()
            if float_compare(delta_qty, 0, precision_rounding=move.product_uom.rounding) > 0:
                _process_increase(move, delta_qty)
            elif float_compare(delta_qty, 0, precision_rounding=move.product_uom.rounding) < 0:
                _process_decrease(move, abs(delta_qty))
        if err:
            raise UserError('\n'.join(err))

    @api.depends('move_line_ids.quantity')
    def _compute_reserved_availability(self):
        """ Fill the `availability` field on a stock move, which is the actual reserved quantity
        and is represented by the aggregated `product_qty` on the linked move lines. If the move
        is force assigned, the value will be 0.
        """
        if not any(self._ids):
            # onchange
            for move in self:
                reserved_availability = sum(move.move_line_ids.mapped('quantity'))
                move.reserved_availability = move.product_id.uom_id._compute_quantity(
                    reserved_availability, move.product_uom, rounding_method='HALF-UP')
        else:
            # compute
            logging.info("begin")
            logging.info(self.env['stock.move.line']._read_group([('move_id', 'in', self.ids)],
                                                              ['move_id'], ['quantity:sum']))
            logging.info("end")
            result = {data[0].id: data[1] for data in  #data['move_id'][0]: data['quantity']
                      self.env['stock.move.line']._read_group([('move_id', 'in', self.ids)],
                                                              ['move_id'], ['quantity:sum'])}#, 'quantity'

            #['move_id', 'product_uom_id'], ['quantity:sum']
            for move in self:
                move.reserved_availability = move.product_id.uom_id._compute_quantity(
                    result.get(move.id, 0.0), move.product_uom, rounding_method='HALF-UP')

    def unlink(self):
        # A stock user could have no RMA permissions, so the ids wouldn't
        # be accessible due to record rules.
        rma_receiver = self.sudo().mapped("rma_receiver_ids")
        rma = self.sudo().mapped("rma_id")
        res = super().unlink()
        rma_receiver.filtered(lambda x: x.state != "cancelled").write(
            {"state": "draft"}
        )
        rma.update_received_state()
        rma.update_replaced_state()
        return res

    def _action_cancel(self):
        res = super()._action_cancel()
        # A stock user could have no RMA permissions, so the ids wouldn't
        # be accessible due to record rules.
        cancelled_moves = self.filtered(lambda r: r.state == "cancel").sudo()
        cancelled_moves.mapped("rma_receiver_ids").write({"state": "draft"})
        cancelled_moves.mapped("rma_id").update_received_state()
        cancelled_moves.mapped("rma_id").update_replaced_state()
        return res

    def _action_done(self, cancel_backorder=False):
        """Avoids to validate stock.move with less quantity than the
        quantity in the linked receiver RMA. It also set the appropriated
        linked RMA to 'received' or 'delivered'.
        """
        for move in self.filtered(lambda r: r.state not in ("done", "cancel")):
            rma_receiver = move.sudo().rma_receiver_ids
            logging.info(move.id)
            logging.info(rma_receiver.product_uom_qty)
            if rma_receiver and move.quantity_done != rma_receiver.product_uom_qty:
                raise ValidationError(
                    _(
                        "The quantity done for the product '%(id)s' must "
                        "be equal to its initial demand because the "
                        "stock move is linked to an RMA (%(name)s)."
                    )
                    % (
                        {
                            "id": move.product_id.name,
                            "name": move.rma_receiver_ids.name,
                        }
                    )
                )
        res = super()._action_done(cancel_backorder=cancel_backorder)
        move_done = self.filtered(lambda r: r.state == "done").sudo()
        # Set RMAs as received. We sudo so we can grant the operation even
        # if the stock user has no RMA permissions.
        to_be_received = (
            move_done.sudo()
            .mapped("rma_receiver_ids")
            .filtered(lambda r: r.state == "confirmed")
        )
        to_be_received.update_received_state_on_reception()
        # Set RMAs as delivered
        move_done.mapped("rma_id").update_replaced_state()
        move_done.mapped("rma_id").update_returned_state()
        return res

    @api.model
    def _prepare_merge_moves_distinct_fields(self):
        """The main use is that launched delivery RMAs doesn't merge
        two moves if they are linked to a different RMAs.
        """
        return super()._prepare_merge_moves_distinct_fields() + ["rma_id"]

    def _prepare_move_split_vals(self, qty):
        """Intended to the backport of picking linked to RMAs propagates the
        RMA link id.
        """
        res = super()._prepare_move_split_vals(qty)
        res["rma_id"] = self.sudo().rma_id.id
        return res

    def _prepare_return_rma_vals(self, original_picking):
        """hook method for preparing an RMA from the 'return picking wizard'."""
        self.ensure_one()
        partner = original_picking.partner_id
        if hasattr(original_picking, "sale_id") and original_picking.sale_id:
            partner_invoice_id = original_picking.sale_id.partner_invoice_id.id
            partner_shipping_id = original_picking.sale_id.partner_shipping_id.id
        else:
            partner_invoice_id = partner.address_get(["invoice"]).get("invoice", False)
            partner_shipping_id = partner.address_get(["delivery"]).get(
                "delivery", False
            )
        return {
            "user_id": self.env.user.id,
            "partner_id": partner.id,
            "partner_shipping_id": partner_shipping_id,
            "partner_invoice_id": partner_invoice_id,
            "origin": original_picking.name,
            "picking_id": original_picking.id,
            "move_id": self.origin_returned_move_id.id,
            "product_id": self.origin_returned_move_id.product_id.id,
            "product_uom_qty": self.product_uom_qty,
            "product_uom": self.product_uom.id,
            "reception_move_id": self.id,
            "company_id": self.company_id.id,
            "location_id": self.location_dest_id.id,
            "state": "confirmed",
        }


class StockRule(models.Model):
    _inherit = "stock.rule"

    def _get_custom_move_fields(self):
        return super()._get_custom_move_fields() + ["rma_id"]


class StockMoveLine(models.Model):
    _inherit = "stock.move.line"

    reserved_qty = fields.Float(
        'Real Reserved Quantity', digits=0, copy=False,
        compute='_compute_reserved_qty', inverse='_set_reserved_qty', store=True)
    reserved_uom_qty = fields.Float(
        'Reserved', default=0.0, digits='Product Unit of Measure', required=True, copy=False)
    qty_done = fields.Float(compute='_compute_qty_done', inverse='_inverse_qty_done',store =True)

    @api.depends('product_id', 'product_uom_id', 'reserved_uom_qty')
    def _compute_reserved_qty(self):
        for line in self:
            line.reserved_qty = line.product_uom_id._compute_quantity(line.reserved_uom_qty, line.product_id.uom_id,
                                                                      rounding_method='HALF-UP')

    def _set_reserved_qty(self):
        """ The meaning of reserved_qty field changed lately and is now a functional field computing the quantity
        in the default product UoM. This code has been added to raise an error if a write is made given a value
        for `reserved_qty`, where the same write should set the `reserved_uom_qty` field instead, in order to
        detect errors. """
        raise UserError(
            _('The requested operation cannot be processed because of a programming error setting the `reserved_qty` field instead of the `reserved_uom_qty`.'))